<?php if (!defined('CORE_DIR')) die('Wrong entry point!');

/* 
 * Copyright 2010 Anton Muraviev a.k.a kuromaisu
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *     http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */


// =============================================================================
// Section: Form
// =============================================================================

Aero::define('AeroForm', AS_CLASS);

/* -----------------------------------------------------------------------------
 * Class: AeroForm
 * -----------------------------------------------------------------------------
 *
 */
class AeroForm
{
	public static $submitted = false;
	public static $registered_commands = array();
	
	protected $action = '';
	protected $multipart = false;
	protected $fieldsets;
	protected $submitted_fields = array();
	
	/*
	 * Constructor: __construct
	 */
	public function __construct($fieldsets)
	{
		$Request = Aero::load('libs/request');
		
		if ($Request->is('post'))
		{
			self::$submitted = true;
		}
		
		$this->fieldsets = $fieldsets;
		
		if (!is_array($fieldsets))
		{
			$this->fieldsets = array($fieldsets);
		}
		
		$fields = array();
		
		// Obtain fields from all fieldsets
		foreach ($this->fieldsets as $fieldset)
		{
			$fields = array_merge($fields,Aero::option('forms.'.$fieldset,array()));
		}
		
		// Setup all fields
		foreach ($fields as $name => $data)
		{
			$data['name'] = $name;
			$control = isset($data['control']) ? $data['control'] : 'text';
			$this->$name = Aero::load('libs/form_controls/'.$control,$data);
			
			if ($control == 'file')
			{
				$this->multipart = true;
			}
		}
		
		// Set submitted fields, if any
		if (isset($_POST['_fields_']) 
			&& isset($_POST['_ts_'])
			&& isset($_POST['_cs_']))
		{
			$url = AeroUri::make($Request->uri->slice());
			$fields = $_POST['_fields_'];
			$unique = $_POST['_ts_'];
			$secret = self::secret();
			$checksum = sha1($url . $fields . $unique . $secret);
			
			$this->submitted_fields = ($checksum == $_POST['_cs_']) ? explode(',',$fields) : array();
		}
		else
		{
			self::$submitted = false;
		}
	}
	
	/*
	 * Method: __call
	 * 
	 * Adds syntactic sugar.
	 * 
	 * Example:
	 * 
	 * (start code)
	 * 	// instead of this:
	 * 	$form->field_name->render('field');
	 * 	// one can use this:
	 * 	$form->field_name('field');
	 * (end)
	 * 
	 * Returns:
	 * 	string
	 * 
	 * Throws:
	 * 	ErrorException	-	if no form field was found
	 */
	public function __call($method, $args)
	{
		if (property_exists($this,$method) && $this->$method instanceof BaseFormControl)
		{
			// FIXME: __call() may not be called to render field.
			$this->submitted_fields[$method] = $method;
			
			// call_user_func_array is nearly twice slower, hence lil' optimization
			return isset($args[0]) ? 
				call_user_func_array(array($this->$method,'render'), $args) :
				$this->$method->render();
		}
		
		throw new ErrorException('Call to undefined method ' . get_class($this) . '::' . 
			$method .'()', 0, E_USER_ERROR);
	}
	
	/*
	 * Method: secret
	 * 
	 * Returns a sercret key to be used by this form and all form controls.
	 * 
	 * Returns:
	 * 	string	-	secret key.
	 */
	public static function secret()
	{
		static $secret = false;
		
		if ($secret)
		{
			return $secret;
		}
		
		if ($secret = Aero::option('form.secret',false))
		{
			return $secret;
		}
		
		trigger_error('Form&#39;s secret is not set. Use <code>Aero::setOption("form.secret", ...your secret value...)</code> to set it.', E_USER_NOTICE);
		
		return $secret = md5(realpath(__FILE__));
	}
	
	/*
	 * Method: isValid
	 *
	 * Checks if all data submitted via this form is valid. Only fields rendered
	 * on the previous page will be validated.
	 *
	 * Returns:
	 *	TRUE or FALSE
	 */
	public function isValid()
	{
		return $this->validate($this->submitted_fields);
	}
	
	/*
	 * Method: validate
	 *
	 * Validates all specified fields.
	 *
	 * Parameters:
	 *	array	-	field names
	 *
	 * Returns:
	 *	TRUE or FALSE
	 */
	public function validate($fields)
	{
		if (!self::$submitted || count(array_intersect_key($_POST, self::$registered_commands)) > 0)
		{
			return false;
		}
		else if (count($fields) == 0)
		{
			return false;
		}
		
		$is_form_valid = true;
		
		foreach ($fields as $field)
		{
			$is_form_valid &= $this->$field->validate();
		}
		
		return $is_form_valid;
	}
	
	/*
	 * Method: open
	 *
	 * Renders `<form>` tag.
	 *
	 * Returns:
	 *	string
	 */
	public function open($attributes = array(), $hidden = array())
	{
		$this->submitted_fields = array();
		
		$attributes['method'] = 'post';
		$attributes['enctype'] = $this->multipart ? 'multipart/form-data' : null;
		
		if (!isset($attributes['action']))
		{
			// Empty actions cause all sorts of issues in Webkit browsers
			// especially when uploading files!
			$attributes['action'] = Aero::load('libs/request')->uri->slice();
		}
		
		$attributes['action'] = AeroUri::make($attributes['action']);
		$this->action = $attributes['action'];
		
		$output = self::tag('form',$attributes) . "\n" .
			// The following hack is required for correct 
			// for submission using keyboard.
			self::tag('input',array(
				'type' => 'submit',
				'name' => 'submit',
				'value' => 'default',
				'style' => 'position:absolute;left:-9999px'
			));
		
		foreach ($hidden as $key => $value)
		{
			$output.= self::tag('input',array(
				'type' => 'hidden',
				'name' => $key,
				'value' => $value
			)) . "\n";
		}
		
		return $output;
	}
	
	/*
	 * Method: close
	 *
	 * Renders `</form>` tag and saves information about all rendered fields to
	 * be used later by <isValid> method.
	 *
	 * Returns:
	 *	string
	 */
	public function close()
	{
		if (count($this->submitted_fields) === 0)
			return '</form>';
		
		$url = $this->action;
		$fields = implode(',',$this->submitted_fields);
		$unique = sha1(uniqid());
		$secret = self::secret();
		
		if (empty($url))
		{
			$Request = Aero::load('libs/request');
			$url = AeroUri::make($Request->uri->slice());
		}
		
		$checksum = sha1($url . $fields . $unique . $secret);
		
		return self::tag('input',array(
				'type' => 'hidden',
				'name' => '_fields_',
				'value' => str_replace('"','\"',$fields) 
			)) . "\n" .
			self::tag('input',array(
				'type' => 'hidden',
				'name' => '_ts_',
				'value' => $unique
			)) . "\n" .
			self::tag('input',array(
				'type' => 'hidden',
				'name' => '_cs_',
				'value' => $checksum
			)) . "\n</form>";
	}
	
	/*
	 * Method: button
	 *
	 * Renders a `<button>` control.
	 *
	 * Returns:
	 *	string
	 */
	public function button($label = 'Submit', $type = 'submit', $name = null, $value = null)
	{
		return self::tag('button',array(
				'type' => $type,
				'name' => $name,
				'value' => $value
			)) . $label . '</button>';
	}
	
	/*
	 * Method: values
	 *
	 * Return all form field values. Can be limited to a specified fieldset.
	 *
	 * Parameters:
	 *	string			-	(optional) escape rule
	 *	string || array	-	(optional) fieldset name(s)
	 *
	 * Returns:
	 *	Array of field name => value pairs.
	 */
	public function values($escape = 'raw', $fieldsets = null)
	{
		$fields = array();
		$values = array();
		
		if (!is_null($fieldsets) && !is_array($fieldsets))
		{
			$fieldsets = array($fieldsets);
		}

		foreach ($this->fieldsets as $fs)
		{
			if (is_null($fieldsets) || in_array($fs, $fieldsets))
			{
				$fields = array_merge($fields,Aero::option('forms.'.$fs,array()));
			}
		}
		
		foreach ($fields as $field => $data)
		{
			if (in_array($field, $this->submitted_fields))
			{
				$values[$field] = $this->$field->value($escape);
			}
		}
		
		return $values;
	}
	
	/*
	 * Method: setDefault
	 *
	 * Sets field default values.
	 *
	 * Parameters:
	 *	object || array	-	default values
	 */
	public function setDefault($values)
	{
		if (is_object($values))
		{
			$values = get_object_vars($values);
		}
		else if (!is_array($values))
		{
			return;
		}

		foreach ($values as $field => $value)
		{
			if (!isset($this->$field) || !$this->$field instanceof BaseFormControl)
			{
				continue;
			}

			$this->$field->setDefault($value);
		}
	}
	
	
	/*
	 * Method: registerCommand
	 * 
	 * Enables any form control to prevent form validation when a $_POST
	 * parameter with such name is passed.
	 * 
	 * Parameters:
	 * 	string	-	command name
	 */
	public static function registerCommand($command)
	{
		$args = func_get_args();
		
		foreach ($args as $value)
		{
			if (is_array($value))
			{
				$value = array_combine($value, $value);
				self::$registered_commands = array_merge(self::$registered_commands,$value);
			}
			else
			{
				self::$registered_commands[$value] = $value;
			}
		}
	}
	
	/*
	 * Method: tag
	 * 
	 * Helper statis method that generates HTML tag
	 * 
	 * Parameters:
	 * 	string	-	tag name
	 * 	array	-	tag parameters
	 */
	protected static function tag($name, $parameters = array())
	{
		$output = '<'.$name;
		
		foreach ($parameters as $attr => $value)
		{
			if (is_null($value)) continue;
			
			$output.=' '.$attr.'="'.BaseFormControl::escape($value).'"';
		}
		
		return $output.'>';
	}
}

/*
 * Constants: Default form field styles
 * 
 * DEFAULT_FORM_FIELD	-	default form field wrapper template
 * DEFAULT_FORM_LABEL	-	default field label template
 * DEFAULT_FORM_HINT	-	default field hint template
 * DEFAULT_FORM_ERROR	-	default field error template
 * DEFAULT_FORM_CONTROL	-	default control template
 * 
 */
define('DEFAULT_FORM_FIELD','<div class="field">[label][control][hint][error]</div>');
define('DEFAULT_FORM_LABEL','<label class="label {label_class}" for="{id}">{label}</label>');
define('DEFAULT_FORM_HINT','<label class="hint {hint_class}" for="{id}">{hint}</label>');
define('DEFAULT_FORM_ERROR','<em class="error {error_class}">{error}</em>');
define('DEFAULT_FORM_CONTROL','<input [attributes] type="{type}" value="{value}">');
define('DEFAULT_FORM_ATTRIBUTES_NOID','name="{name}" disabled="{disabled}" class="{control_class}"');
define('DEFAULT_FORM_ATTRIBUTES','id="{id}" '.DEFAULT_FORM_ATTRIBUTES_NOID);

/* -----------------------------------------------------------------------------
 * Class: BaseFormControl
 * -----------------------------------------------------------------------------
 *
 * Base form control class that provides functionality.
 *
 */
abstract class BaseFormControl
{
	// An `array` of modifiable properties
	protected $properties = array('name','label', 'hint', 'disabled', 'multiple', 'label_class', 'control_class', 'error_class', 'hint_class');
	
	public $label; // `string` -- control label
	public $hint; // `string` -- control hint 
	public $error; // `string` -- error message
	public $errors; // `array` -- an associtive array of constraints to error messages
	public $constraints; // `AeroConstraints` -- validation constraints
	
	public $disabled; // `bool` -- whether field is disabled
	protected $multiple; // `bool` -- `true` if more than one instance can be used

	public $label_class; // `string` -- additional label class(es)
	public $control_class; // `string` -- additional control class(es)
	public $error_class; // `string` -- additional error class(es)
	public $hint_class; // `string` -- additional hint class(es)
	
	protected $name; // `string` -- usually matches name attribute of the control
	protected $name_prefix; // `string` -- prefix used in the name attribute
	protected $name_suffix; // `string` -- suffix used in the name attribute

	protected $type; // `string` -- usually matches type attribute of the control
	protected $style = 'default'; // `string` -- different families of controls may use common styles.
	
	protected $value; // `misc` -- current value
	protected $default; // `misc` -- default value

	// An associative `array` of snippet names and templates that
	// this control exposes for styling.
	protected $snippets = array(
		'control' => DEFAULT_FORM_CONTROL,
		'label' => DEFAULT_FORM_LABEL,
		'error' => DEFAULT_FORM_ERROR,
		'hint' => DEFAULT_FORM_HINT,
		'field' => DEFAULT_FORM_FIELD,
		'attributes' => DEFAULT_FORM_ATTRIBUTES
	);
	
	// An `array` of snippet names that will be re-rendered for each control.
	protected $dynamic_snippets = array();
	
	// Properties: Private properties
	// 
	// Used by rendering methods.
	// 
	private $offset = 0;
	private $new = false;
	
	static private $resolved_snippets = array();
	static private $issued_offsets;

	/*
	 * Constructor: __construct
	 */
	public function __construct($params = array())
	{
		foreach ($params as $key => $value)
		{
			if (in_array($key, $this->properties) && property_exists($this,$key))
			{
				$this->$key = $value;
			}
		}
		
		$this->type = isset($params['control']) ? $params['control'] : 'text';
		
		// Set constraints object
		$this->constraints = Aero::load('libs/constraints',isset($params['constraints']) ? $params['constraints'] : '');
		
		// If label is empty make one from control name
		if (empty($this->label))
		{
			$this->label = ucfirst(str_replace(array('-','_'),' ',$this->name));
		}
		
		if (isset($params['default']))
		{
			$this->setDefault($params['default']);
		}
		
		$this->_initialize();
	}

	/*
	 * Method: _initialize
	 * 
	 * Initializes control with value.
	 */
	protected function _initialize()
	{
		if (isset($_POST[$this->name]))
		{
			$this->value =& $_POST[$this->name];
		}
		else if ($this->multiple)
		{
			$this->value = array();
		}
		else
		{
			$this->value = '';
		}
	}

	/*
	 * Method: validate
	 *
	 * Allows control to validate itself. 
	 * 
	 * Control is responsible for setting error message.
	 * 
	 * Returns:
	 * 	`bool`
	 */
	public function validate()
	{
		Aero::import('libs/i18n');
		
		$result = $this->constraints->validate($this->value('raw'));
		
		if (true === $result)
		{
			return true;
		}
		
		if (!$this->multiple)
		{
			$result = array($result);
		}
		
		foreach ($result as $index => $_result)
		{
			if ($_result === true)
			{
				continue;
			}
			
			$args = $_result[2];
			array_unshift($args, $this->label);
			
			if (isset($this->errors[$_result[1]]))
			{
				$result[$index] = ae(null, $this->errors[$_result[1]], $args);
			}
			else
			{
				$result[$index] = ae('constraints', $_result[1], $args);
			}
		}
		
		$this->error = ($this->multiple) ? $result : array_pop($result);
		
		return false;
	}
	
	/*
	 * Method: setDefault
	 *
	 * Sets default field value(s).
	 */
	public function setDefault($value)
	{
		$this->default = $value;
		
		if ($this->multiple && !empty($this->default) && !is_array($this->default))
		{
			$this->default = array($this->default);
		}
	}
	
	/*
	 * Method: value
	 *
	 * Returns current field value(s).
	 */
	public function value($escape = 'escaped')
	{
		$value = AeroForm::$submitted ? $this->value : $this->default;
		
		if ($escape == 'escaped')
		{
			return self::escape($value);
		}
		else if (empty($escape) || $escape == 'raw')
		{
			return $value;
		}
		
		// We need a custom sanitizer
		$sanitizer = Aero::option(
			'form.sanitizers.'.$escape.'.path', 
			Aero::option(
				'form.sanitizers.default.path', 
				false));
		
		if (!empty($sanitizer))
		{
			$sanitizer = Aero::load($sanitizer);
		}
		
		if (is_object($sanitizer) && method_exists($sanitizer,'parse'))
		{
			return $sanitizer->parse($value);
		}
		
		trigger_error('Could not initialize sanitizer for escape method: '.$escape.'. ', E_USER_ERROR);
	}
	
	public function hasErrors()
	//
	// Returns:
	// * `true` --  if form has error message(s) set.
	// * `false` -- if no error message(s) was(were) set.
	// 
	{
		if ($this->error === true || is_null($this->error))
		{
			return false;
		}
		else if (is_array($this->error))
		{
			$unique = array_unique($this->error);
			
			if (count($unique) == 1 && array_pop($unique) === true)
			{
				return false;
			}
		}
		var_dump($this->error);
		return true;
	}
	
	/*
	 * Method: escape
	 * 
	 * Escapes value so that it can be used in a form.
	 * 
	 * If the value is (nested) array, this method is applied to all its elements.
	 * 
	 * Parameters:
	 * 	misc	-	any scalar or array will do;
	 * 
	 */
	public static function escape($value)
	{
		if (is_array($value))
		{
			return array_map(array('BaseFormControl', 'escape'), $value);
		}
		
		// htmlspecialchars() breaks all HTML entities, so we need to fix them
		return preg_replace('~&amp;(#\d+;|\w+;)~','&\1',htmlspecialchars($value));
	}

	/*
	 * Method: render
	 *
	 * Renders the whole field or its label, control, error message or hint.
	 * 
	 * Parameters:
	 * 	string	-	snippet name (e.g. 'field', 'label', 'control', 'error', 'hint').
	 * 
	 * Returns:
	 * 	string	-	final html code.
	 */
	public function render($what = 'field')
	{
		$class = get_class($this);
		$control_id = self::_getFieldIdAndUpdateOffsets($what);
		
		if (!$this->new && isset($this->rendered_snippets[$what]))
		{
			return $this->rendered_snippets[$what]."\n";
		}
		
		// Render static snippets
		if (!isset(self::$resolved_snippets[$class]))
		{
			self::$resolved_snippets[$class] = array();
			$templates = Aero::option('form.'.$this->style, $this->snippets);
			
			// $templates may contain junk records; cleaning...
			$templates = array_intersect_key($templates, $this->snippets);
			
			$this->_resolveTemplateHeirarchy($templates,self::$resolved_snippets[$class]);
		}
		
		if (empty($this->snippets[$what]) || empty(self::$resolved_snippets[$class][$what]))
		{
			return ''; // Attempt to render a non-existant snippet
		}
		
		
		$variables = array(
			'id' => $control_id,
			'type' => $this->type,
			'name' => $this->name_prefix . $this->name . $this->name_suffix . ($this->multiple ? '['.$this->offset.']' : ''),
			'offset' => $this->offset,
			'raw_name' => $this->name,
			'label' => $this->_fieldProperty('label',false,$this->offset),
			'hint' => $this->_fieldProperty('hint',false,$this->offset),
			'error' => $this->_fieldProperty('error',false,$this->offset),
			'disabled' => $this->disabled ? 'disabled' : '',
			'multiple' => $this->multiple ? 'multiple' : '',
			'value' => $this->_fieldProperty('value',true,$this->offset),
			'raw_value' => $this->_fieldProperty('value',false,$this->offset),
			'control_class' => $this->_class('control'),
			'label_class' => $this->_class('label'),
			'hint_class' => $this->_class('hint'),
			'error_class' => $this->_class('error')
		);
		
		if (!empty($variables['error']))
		{
			$variables['hint'] = null;
		}
		
		$snippets = self::$resolved_snippets[$class];
		
		// Render dynamic snippets
		if (is_array($this->dynamic_snippets) && count($this->dynamic_snippets) > 0)
		{
			$_snippets = array();
			
			foreach ($this->dynamic_snippets as $_name)
			{
				$method = '_'.str_replace('_','',$_name).'snippet';
				
				if (method_exists($this,$method))
				{
					$snippets[$_name] = $this->$method($variables);
				}
			}
			
			$this->_resolveTemplateHeirarchy($snippets,$_snippets);
			
			$snippets = $_snippets;
		}
		
		$this->_modifyVariables($variables);
		
		// Prepare the variables
		$_variables = array();
		
		foreach ($variables as $key => $var)
		{
			if (!is_array($var))
			{
				$_variables['{'.$key.'}'] = $var;
			}
		}
		
		$result = str_replace(array_keys($_variables), array_values($_variables), $snippets);
		$result = preg_replace('~(disabled|checked|selected)=(""|\'\')~', '', $result);

		$this->rendered_snippets = $result;
		
		// If error, hint or other variable is not set, remove the snippet with the same name.
		foreach ($this->rendered_snippets as $_name => $_value)
		{
			if (array_key_exists($_name, $variables) && is_null($variables[$_name]))
			{
				$this->rendered_snippets['field'] = str_replace($_value, '', $this->rendered_snippets['field']);
				$this->rendered_snippets[$_name] = '';
			}
		}
		
		return $this->rendered_snippets[$what] . "\n";
	}
	
	/*
	 * Method: _resolveTemplateHeirarchy
	 * 
	 * Resolves a heirarchy of templates using each other via
	 * [template_name] placeholders.
	 * 
	 * Parameters:
	 * 	array	-	snippet names => templates;
	 * 	array	-	resolved snippet names => values.
	 */
	protected function _resolveTemplateHeirarchy(&$snippets, &$ready)
	{
		$keys = array_keys($snippets);
		
		while (!is_null($snippet = array_pop($snippets)))
		{
			$name = array_pop($keys);
			
			if (preg_match_all('~\[('.implode('|',$keys).')\]~', $snippet, $sub_names) > 0)
			{
				$sub_names = $sub_names[1];
				$sub_snippets = array();
				
				foreach ($sub_names as $sub_name)
				{
					if (isset($snippets[$sub_name]))
					{
						$sub_snippets[$sub_name] = $snippets[$sub_name];
						unset($snippets[$sub_name]);
					}
				}
				
				$keys = array_keys($snippets);
				$this->_resolveTemplateHeirarchy($sub_snippets,$ready);
			} 
			
			$_rendered = array();
			
			foreach ($ready as $key => $value)
			{
				$_rendered['['.$key.']'] = $value;
			}
			
			$ready[$name] = str_replace(array_keys($_rendered), array_values($_rendered), $snippet);
		}
	}
	
	/*
	 * Method: _modifyVariables
	 * 
	 * Allows child classes to change existing or add new field template variables.
	 */
	protected function _modifyVariables(&$vars) {}
	
	/*
	 * Method: _class
	 * 
	 * Parameters:
	 * 	string	-	element name (i.e. control or label);
	 * 	string	-	(optional) aditional class names.
	 * 
	 * Returns:
	 * 	string	-	space-separated classes for this element;
	 * 	null	-	if element has no classes.
	 */
	protected function _class($element, $class = '')
	{
		if ($class)
		{
			$class.= ' ';
		}

		if (!empty($this->{$element.'_class'}))
		{
			$class.= ' ' . $this->{$element.'_class'};
		}

		$class = trim($class);

		return $class ? $class : null;
	}
	
	/*
	 * Method: _getFieldIdAndUpdateOffsets
	 * 
	 * Returns unique id for given field element.
	 * 
	 * It also updates all the field offset and $new flag.
	 * 
	 * Parameters:
	 * 	string	-	element name (control, label, etc);
	 * 
	 * Returns:
	 * 	string	-	unique id
	 */
	protected function _getFieldIdAndUpdateOffsets($element)
	{
		$this->new = false;
		$old_offset = $this->offset;
		
		if (!isset(self::$issued_offsets[$this->name]))
		{
			$this->new = true;
			self::$issued_offsets[$this->name] = array($element => 0);
		}
		
		$_issued =& self::$issued_offsets[$this->name];
		
		if (!isset($_issued[$element]))
		{
			$_issued[$element] = 0;
		}
		
		$_issued[$element]++;
		$this->offset = max($_issued) - 1;
		
		// If offset has changed
		if ($this->offset > $old_offset)
		{
			$this->new = true;
		}
		
		$sep = Aero::option('form.id_separator','_');
		$id = Aero::option('form.id_prefix','') . $this->name . $sep;
		
		if ($this->offset > 0)
		{
			$id.= $this->offset . $sep;
		}
		
		$id.= Aero::option('form.id_suffix','field');
		
		return $id;
	}
	
	/*
	 * Method: _fieldValue
	 * 
	 * Returns current field value.
	 * 
	 * If the form is not submitted, returns default value.
	 * 
	 * Parameters:
	 * 	bool	-	whether to escape the value or not;
	 * 	int		-	(optional) index to be used if value is an array.
	 * 
	 * Returns:
	 * 	scalar	-	if <BaseFormControl::$multiple> is FALSE or index is provided;
	 * 	array	-	otherwise.
	 */
	protected function _fieldProperty($prop = 'value', $escape = true, $index = null)
	{
		if ($prop === 'value' && !AeroForm::$submitted)
		{
			$prop = 'default';
		}
		
		$value = $this->$prop;
		
		if ($this->multiple)
		{
			if (is_array($value))
			{
				switch ($prop)
				{
					case 'value':
					case 'default':
					case 'error':
						$value = !is_null($index) && isset($value[$index]) ? $value[$index] : '';
						break;
					
					default:
						$value = !is_null($index) && isset($value[$index]) ? $value[$index] : array_pop($value);
						break;
				}
			}
		}
		
		if ($escape)
		{
			return self::escape($value);
		}
		
		return $value;
	}
}


/*
 * Constants: Default form option styles
 * 
 * DEFAULT_FORM_OPTION			-	default option template;
 * DEFAULT_FORM_OPTION_GROUP	-	default option group template.
 * 
 */
define('DEFAULT_FORM_OPTION','[{label},{value},{selected}]');
define('DEFAULT_FORM_OPTION_GROUP','[{label}:{options}]');

/* -----------------------------------------------------------------------------
 * Class: BaseOptionFormControl
 * -----------------------------------------------------------------------------
 *
 * This class provides basic functionality for option-based controls
 *
 */
abstract class BaseOptionFormControl extends BaseFormControl
{
	public $options = array();
	
	protected $dynamic_snippets = array('options');
	protected $option_selected = 'selected';
	protected $option_template = DEFAULT_FORM_OPTION;
	protected $group_template = DEFAULT_FORM_OPTION_GROUP;
	
	public function __construct($params = array())
	{
		$this->properties = array_merge($this->properties, array('options'));
		
		parent::__construct($params);
	}
	
	protected function _optionsSnippet(&$variables)
	{
		$current_value = $this->_fieldProperty('value', true, $variables['offset']);
		$options = self::escape($this->options);
		$output = '';
		
		foreach ($options as $key => $value)
		{
			if (is_array($value))
			{
				$options = '';
				
				foreach ($value as $k => $v)
				{
					$options.= $this->_option($v,$k,$current_value);
				}
				
				$output.= str_replace(
					array('{label}', '{options}'),
					array($key,$options),
					$this->group_template
				);
			}
			else
			{
				$output.= $this->_option($value,$key,$current_value);
			}
		}
		
		return $output;
	}
	
	protected function _option($label, $value, $current_value)
	{
		$selected = $this->_isSelected($value, $current_value);

		return str_replace(
			array('{label}','{value}','{'.$this->option_selected.'}'),
			array($label, $value, $selected),
			$this->option_template
		);
	}
	
	/*
	 * Method: _isSelected
	 * 
	 * Parameters:
	 * 	scalar	-	an option value;
	 * 	misc	-	current value(s).
	 * 
	 * 
	 * Returns:
	 * 	'selected'	-	if this options is currently selected;
	 * 	''			-	otherwise.
	 */
	protected function _isSelected($option, $value)
	{
		if (is_array($value))
		{
			$bool = in_array($option, $value);
		}
		else
		{
			$bool = ($value == $option);
		}
		
		return $bool ? $this->option_selected : '';
	}
}
